/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using DaveSexton.DocProject.Engine;
using System.IO;
using System.Net;
using System.Threading;
using System.Windows.Forms;
using DaveSexton.DocProject.Sandcastle;

namespace DaveSexton.DocProject.DeploymentSandcastle
{
	internal delegate IEnumerable<DeploymentContent> GetContentCallback(DeploymentContentTypes contentTypes, DeploymentContext context);

	internal sealed class DeploymentSandcastleBuildStep : BuildStep<SandcastleBuildEngine>
	{
		#region Public Properties
		public override string Name
		{
			get
			{
				return "Deployment";
			}
		}

		public new DeploymentSandcastleBuildEngine Engine
		{
			get
			{
				return (DeploymentSandcastleBuildEngine) base.Engine;
			}
		}

		public bool DeployDocSiteContentOnly
		{
			set
			{
				deployDocSiteContentOnly = value;
			}
		}
		#endregion

		#region Private / Protected
		private readonly GetContentCallback getContent;
		private readonly EventWaitHandle cancelDeploymentHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
		private bool deployDocSiteContentOnly;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="DeploymentSandcastleBuildStep" /> class.
		/// </summary>
		public DeploymentSandcastleBuildStep(DeploymentSandcastleBuildEngine engine, GetContentCallback getContent)
			: base(engine)
		{
			if (getContent == null)
				throw new ArgumentNullException("getContent");

			this.getContent = getContent;

			IncludeInPartialBuild = true;
		}
		#endregion

		#region Methods
		public IEnumerable<DeploymentContent> GetContent(bool partialBuildItemsOnly)
		{
			DeploymentSandcastleProjectOptions options = Engine.Options;

			Uri target = options.DeploymentLocation;

			if (target == null)
				return new System.Collections.ObjectModel.ReadOnlyCollection<DeploymentContent>(new List<DeploymentContent>(0));

			DeploymentContext context = new DeploymentContext(Engine, target);
			context.PartialBuildItemsOnly = partialBuildItemsOnly;
			context.DeployDocSiteContentOnly = deployDocSiteContentOnly;

			return getContent(options.DeploymentContent, context);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		protected override void ExecuteInternal(BuildContext context)
		{
			cancelDeploymentHandle.Reset();

			context.TraceLine();

			bool success = true, attempted = false, canceled = false;

			try
			{
				if (Engine.Options.DeploymentEnabled)
				{
					string status = "Starting deployment to " + Engine.Options.DeploymentLocation.ToString() + "...";

					context.TraceLine(status);
					context.SetProgress(status, 0, 100);

					List<DeploymentContent> deployed = new List<DeploymentContent>();
					List<DeploymentContent> content = new List<DeploymentContent>(GetContent(context.BuildType == BuildType.Partial));

					int total = content.Count, current = 0;

					Application.DoEvents();

					if (cancelDeploymentHandle.WaitOne(0, false))
						canceled = true;
					else
					{
						attempted = total > 0;

						context.TraceLine("Deploying " + total + " item(s)...");

						foreach (DeploymentContent item in content)
						{
							if (item == DeploymentContent.Empty)
								continue;

							Application.DoEvents();

							if (cancelDeploymentHandle.WaitOne(0, false))
							{
								canceled = true;
								break;
							}

							if (!deployed.Contains(item))
							{
								Uri source = item.Source;
								Uri target = item.Target;

								deployed.Add(item);

								context.SetProgress("Deploying " + source.ToString() + "...", current * 100 / total, 100);

								context.TraceLine();
								context.TraceLine("Deploying " + source.ToString());
								context.TraceLine("       to " + System.Web.HttpUtility.UrlDecode(
									((target.IsAbsoluteUri && (target.IsFile || target.IsUnc)) ? target.LocalPath : target.ToString())));

								try
								{
									Deploy(item, context);
								}
								catch (FileNotFoundException)
								{
									context.AddWarningToList("Sandcastle/Deployment", "Mssing " + source, "Missing source.");
									success = false;
								}
								catch (Exception ex)
								{
									context.AddErrorToList("Sandcastle/Deployment", ex.Message, true);
									success = false;
								}
							}

							current++;
						}
					}
				}
				else
				{
					context.TraceLine("Deployment is not enabled.");
				}
			}
			finally
			{
				// reset progress before setting status since it will set status to empty
				context.SetProgress(null, 100, 100);

				if (attempted)
				{
					string status = "Deployment " + ((canceled) ? "canceled" : ((success) ? "completed successfully" : "failed")) + ".";

					context.SetStatus(status);

					context.TraceLine();
					context.TraceLine(status);
				}
				else
					context.SetStatus("Deployment skipped.");
			}
		}

		private void Deploy(DeploymentContent content, BuildContext context)
		{
			string source = System.Web.HttpUtility.UrlDecode(content.Source.AbsolutePath);
			Uri target = content.Target;

			if (!target.IsAbsoluteUri)
			{
				string relativeTarget = System.Web.HttpUtility.UrlDecode(target.ToString());
				string absoluteTarget = Path.Combine(Engine.Settings.ProjectDirectory, relativeTarget);

				DeployLocal(source, absoluteTarget, context);
			}
			else if (target.IsFile || target.IsUnc)
				DeployLocal(source, target.LocalPath, context);
			else
				DeployWeb(source, target, context);
		}

		private void DeployLocal(string source, string target, BuildContext context)
		{
			bool isDir = PathFormat.IsDirectory(target);
			string dir = (isDir) ? target : Path.GetDirectoryName(target);

			if (!Directory.Exists(dir))
				Directory.CreateDirectory(dir);

			if (isDir)
			{
				foreach (string file in Directory.GetFiles(source, "*.*", SearchOption.AllDirectories))
				{
					Application.DoEvents();

					if (cancelDeploymentHandle.WaitOne(0, false))
						return;

					Uri sourceUri = new Uri(source);
					string relativeSoruce = System.Web.HttpUtility.UrlDecode(sourceUri.MakeRelativeUri(new Uri(file)).ToString());
					string fullTarget = Path.Combine(target, relativeSoruce);

					//context.TraceLine("Copying " + file);
					//context.TraceLine("     to " + fullTarget);

					dir = Path.GetDirectoryName(fullTarget);

					if (!Directory.Exists(dir))
					{
						context.TraceLine("Creating target directory: {0}", dir);

						Directory.CreateDirectory(dir);
					}

					File.Copy(file, fullTarget, true);
				}
			}
			else
				File.Copy(source, target, true);
		}

		private void DeployWeb(string source, Uri target, BuildContext context)
		{
			bool isDir = PathFormat.IsDirectory(System.Web.HttpUtility.UrlDecode(target.AbsolutePath));

			using (WebClient client = new WebClient())
			{
				InitializeWebClient(client);

				DateTime start = DateTime.Now;
				bool completed = false, canceling = false;
				Exception asyncException = null;

				client.UploadProgressChanged += delegate(object sender, UploadProgressChangedEventArgs e)
				{
					TimeSpan progressTime = DateTime.Now - start;

					if (progressTime.Seconds >= 5)
					// trace every 5 seconds; if the UploadProgressChanged event is raised less frequently 
					// then the trace will be at the interval at which the event is raised.
					{
						string label = string.Format(System.Globalization.CultureInfo.CurrentCulture,
							"Uploading " + source + "; {0} of {1} bytes sent...", e.BytesSent, e.TotalBytesToSend);

						context.SetProgress(label, e.ProgressPercentage, 100);

						start += progressTime;
					}
				};

				client.UploadFileCompleted += delegate(object sender, UploadFileCompletedEventArgs e)
				{
					if (!e.Cancelled && e.Error != null)
						asyncException = e.Error;

					completed = true;
				};

				if (isDir)
				{
					const int maxSimultaneousUploads = 5;
					int current = 0;

					foreach (string file in Directory.GetFiles(source, "*.*", SearchOption.AllDirectories))
					{
						Application.DoEvents();

						if (asyncException != null || canceling || cancelDeploymentHandle.WaitOne(0, false))
						{
							if (current > 0)
								client.CancelAsync();

							break;
						}

						Uri sourceUri = new Uri(source);
						string relativeSoruce = System.Web.HttpUtility.UrlDecode(sourceUri.MakeRelativeUri(new Uri(file)).ToString());
						Uri fullTarget = new Uri(target, relativeSoruce);

						context.TraceLine("Uploading file {0} to {1}...", sourceUri, fullTarget.ToString());

						client.UploadFileAsync(fullTarget, source);

						current++;

						if (current == maxSimultaneousUploads)
						{
							while (client.IsBusy)
							{
								if (!canceling && cancelDeploymentHandle.WaitOne(0, false))
								{
									canceling = true;
									client.CancelAsync();
								}

								Application.DoEvents();
							}

							current = 0;
						}
					}
				}
				else
					client.UploadFileAsync(target, source);

				while (!completed)
				{
					if (!canceling && cancelDeploymentHandle.WaitOne(0, false))
					{
						canceling = true;
						client.CancelAsync();
					}

					Application.DoEvents();
				}

				if (asyncException != null)
					throw asyncException;
			}
		}

		private void InitializeWebClient(WebClient client)
		{
			if (!(client.UseDefaultCredentials = Engine.Options.DeploymentUseDefaultCredentials))
				client.Credentials = new NetworkCredential(Engine.Options.DeploymentUserName, Engine.Options.DeploymentPassword);

			client.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);

			Uri proxy = null;

			if (Engine.Options.DeploymentUseProxy && (proxy = Engine.Options.DeploymentProxy) != null)
			{
				client.Proxy = new WebProxy(proxy, true);

				if (Engine.Options.DeploymentUseDefaultProxyCredentials)
					client.Proxy.Credentials =
						new NetworkCredential(Engine.Options.DeploymentProxyUserName, Engine.Options.DeploymentProxyPassword);
			}
		}

		protected override void CancelInternal(DaveSexton.DocProject.BuildContext context)
		{
			cancelDeploymentHandle.Set();
		}

		protected override void Dispose(bool disposing)
		{
			try
			{
				if (disposing)
					cancelDeploymentHandle.Close();
			}
			finally
			{
				base.Dispose(disposing);
			}
		}

		public override string ToString()
		{
			if (Engine.Options.DeploymentEnabled)
			{
				Uri target = Engine.Options.DeploymentLocation;

				return "Deploy " + Engine.Options.DeploymentContent.ToString() + " to " +
					((target.IsFile || target.IsUnc) ? target.LocalPath : target.ToString()) + ".";
			}
			else
				return "Deployment is not enabled.";
		}
		#endregion
	}
}
